/**
 * @file    array_apply.h
 * @author  Ying-Po Liao <lrobin@neo.tamu.edu>
 * @version 1.0
 *
 * @section This file is mainly for the namespace "array_helper" included several
 *	helper classes for the implementations of array subscripting/slicing/resizing.
 *      1) array_slice: array subscrpting or slicing
 *      2) array_resize: array resizing
 *
 * CSCE-689 Design Using C++
 * Final Project
 */

#ifndef ARRAY_HELPER
#define ARRAY_HELPER

template <typename, unsigned int> class array;

namespace array_helper
{

//! calculate p_zero_offset with an initial value init_val
template <typename num_T, unsigned int DIM, typename int_T>
void init_pzero_offset( const array<num_T, DIM>& arr, int_T& p_zero_offset, int_T init_val )
{
	p_zero_offset = init_val;
	auto stride = arr.stride();
	auto base = arr.lbound();
	for( unsigned int dim = 0; dim < DIM; dim++ )
		p_zero_offset -= base[dim] * stride[dim];
}

//! calculate p_zero_offset without initial values
template <typename num_T, unsigned int DIM, typename int_T>
void init_pzero_offset( const array<num_T, DIM>& arr, int_T& p_zero_offset )
{
	init_pzero_offset( arr, p_zero_offset, int_T(0) );
}

/**
 *  @brief  implementation class to unpack arguments and slice array
 *  @tparam source_dim current dimension referred to the unsliced array
 *  @tparam n_dim total dimensional in unsliced array
 *  @tparam tuple_type tuple type shadowing to the slcing arguments
 *  @tparam return_type return array type (sliced)
 *  @tparam array_type original array type (unsliced)
 *
 *  This is the class to implement array slicing using recursive template. 
 *  The variadic arguments such as (int, range, int, int, range...) are
 *  packed as a shadow tuple object. By recursive template, the argument will be
 *  unpacked along with the increment of dimension index denoted as "source_dim"
 *  According to two types of each unpacked argument, int or range, two "unpack" 
 *  interfaces will be invoked correspondingly.
 */
template <int source_dim, int n_dim, class tuple_type, class return_type, class array_type>
struct unpack_and_slice
{
	inline static void unpack( int i, int& set_dim, 
		return_type& new_array, array_type& old_array,
		tiny_vector<int, array_type::n_dim>& dim_map, int& data_off )
	{
		dim_map[ source_dim ] = -1;
		data_off += i * old_array.stride_[ source_dim ];
	}
		
	inline static void unpack( range r, int& set_dim, 
		return_type& new_array, array_type& old_array,
		tiny_vector<int, array_type::n_dim>& dim_map, int& data_off )
	{		
		dim_map[ source_dim ] = set_dim;
		new_array.shape_[ set_dim ] = old_array.shape_[ source_dim ];
		new_array.stride_[ set_dim ] = old_array.stride_[ source_dim ];
		new_array.storage_.set_base( set_dim, old_array.storage_.get_base( source_dim ) );	
		new_array.p_zero_offset_ = 0;
		new_array.slice( set_dim, r );
		data_off += new_array.p_zero_offset_;
		++set_dim;
	}

	inline static void go( const tuple_type& idx, int& set_dim, 
		return_type& new_array, array_type& old_array,
		tiny_vector<int, array_type::n_dim>& dim_map, int& data_off )
	{
		unpack( std::get<source_dim>(idx), set_dim, new_array, old_array, dim_map, data_off );
		
		unpack_and_slice<source_dim+1, n_dim, tuple_type, return_type, array_type>
		::go( idx, set_dim, new_array, old_array, dim_map, data_off );
	}
};

template <int n_dim, class tuple_type, class return_type, class array_type>
struct unpack_and_slice<n_dim, n_dim, tuple_type, return_type, array_type>
{
	inline static void go( const tuple_type& idx, int& set_dim, 
		return_type& new_array, array_type& old_array,
		tiny_vector<int, array_type::n_dim>& dim_map, int& data_off ) 
	{}
};

/**
 *  @brief  implementation class for array subscripting or slicing
 *  @tparam array_type original array type (unsliced)
 *  @tparam ...ID_types variadic template arguments of ID types
 *
 *  There are three versions of array_slice:
 *  1) array_slice: general type. Never used
 *
 *  2) array_slice<array_type, T>: one-argument version. This is the specialized 
 *  version for one-argument subscripting / slicing. In this case, only two 
 *  possibilities may be occured: 
 *  (a) a int tiny_vector, such as a( {1,4,2} ), which implies array "subscripting" 
 *  and returning one single value. 
 *  (b) a range tiny_vector, such as a( {range(3,4), range(2,5), range(1,6)} ), 
 *  which implies array "slicing" and returning a new sliced sub-array that shares 
 *  the same data set with the original one.
 *
 *  3) array_slice<array_type, T, ID_types...>: many-argument version. This is the
 *  specialized version for many-argument array subscripting / slicing. There are
 *  only three possibilities may be occured: 
 *  (a) all are integers, such as a(4, 5, 7, 2), which is totally the same as case
 *  (2-a) so we just simply pack all arguments as a int tine_vector and forward to that
 *  (b) all are ranges, such as a(range(3,4), range(2,5), range(1,4)), which is totally
 *  the same as case (2-b) so we pack all arguments as a range tiny_vector and forward.
 *  (c) int and range mixed, such as a(4, range(2,6), 5), which implies a degenerated
 *  slicing. The new sliced array will have the number of dimensions smaller than the 
 *  original one (if all ranges, the sub-array with the same dimensions). So we pack 
 *  all arguments as a shadow tuple object, and forward to unpack_and_slice to unpack 
 *  and slice the degenerated sub-array using recursive template.
 */
template <typename array_type, typename... ID_types>
struct array_slice;

template <typename array_type, typename T>
struct array_slice<array_type, T>
{
	//! all ids are ints --> return a value (const or non-const)
	inline static typename std::conditional<
		std::is_const<array_type>::value, 
		typename array_type::num_type, typename array_type::num_type&
	>::type
	get( array_type& arr, const tiny_vector<int, array_type::n_dim>& id )
	{
		return (*arr.data_p_)[ arr.p_zero_offset_ + 
			dot_meta<array_type::n_dim, 0>::get( id, arr.stride_ ) ];		
	}
	
	//! all ids are range --> slice a subarray with same dimensions
	inline static array_type 
	get( array_type& arr, const tiny_vector<range, array_type::n_dim>& new_slice )
	{
		return array_type( arr, new_slice );
	}
	
	typedef decltype( get(std::declval<array_type&>(), std::declval<T>()) ) return_type;
};

template <typename array_type, typename T, typename... ID_types>
struct array_slice<array_type, T, ID_types...>
{
	typedef type_list<T, ID_types...> id_types;
	
	typedef typename array_type::num_type num_type;
	static constexpr unsigned int n_dim = array_type::n_dim;
	
	//! check the length of types (arguments)
	static constexpr bool len_check = (id_types::num_types == n_dim);
	static_assert( len_check, "wrong length of indices");
	
	//! check (num of int) + (num of range) == dimension
	static constexpr unsigned int num_of_int   = num_of_type_inside<int, id_types>::value;
	static constexpr unsigned int num_of_slice = num_of_type_inside<range, id_types>::value;
	static constexpr bool check_equal = (num_of_int + num_of_slice) == n_dim;
	static_assert( check_equal, "indexing sould only be int or range");

	//! check if all are integers or all are ranges
	static constexpr bool all_are_ints = all_same_as_type<int, id_types>::value;
	static constexpr bool all_are_rngs = all_same_as_type<range, id_types>::value;
	typedef typename 
		std::integral_constant<bool, all_are_ints || all_are_rngs>::type 
	all_ints_or_rngs;
	
	//! if all are int. or all are ranges, then deduce the uniform return type
	typedef decltype( 
		array_slice<array_type, T>::get(std::declval<array_type&>(), std::declval<T>()) 
	) uniform_return_type;
	
	//! deduce return type, (1) uniform return type if all int. or ranges. (2) degenerated array type
	typedef typename std::conditional<
		all_ints_or_rngs::value, uniform_return_type, array<num_type, n_dim - num_of_int>
	>::type return_type;
	
	//! <<IMPL.1>> [case 1] all are integers or [case 2] all are ranges
	struct uniform_get_impl
	{
		inline static return_type get( array_type& arr, T id1, ID_types... ids )
		{  return array_slice<array_type, T>::get( arr, {id1, ids...}  );  }
	};
	
	//! <<IMPL.2>> [case 3] generate degenerated array due to mixed int & range indexing type
	struct degenerated_get_impl
	{
		static constexpr unsigned int new_dim = return_type::n_dim;

		inline static return_type get( array_type& arr, T id1, ID_types... ids )
		{	
			return_type new_arr;
			new_arr.data() = arr.data();
			new_arr.zero_offset() = 0;
			int set_dim = 0;
			tiny_vector<int, n_dim> dim_map;
			int data_off = num_of_int >= 1 ? arr.zero_offset() : 0;
			
			typedef typename id_types::tuple_type shadow_tuple;
			shadow_tuple tp( id1, ids... );
			unpack_and_slice<0, n_dim, shadow_tuple, return_type, array_type>
			::go( tp, set_dim, new_arr, arr, dim_map, data_off );

			new_arr.zero_offset() = data_off;
			
			unsigned int j = 0;
			for ( unsigned int i = 0; i < n_dim; ++i)
			{
				auto old_order = arr.storage().get_storage_order(i);
				if ( dim_map[ old_order ] != -1 )
				{
					new_arr.storage().set_storage_order( j, dim_map[ old_order ]);
					++j;
				}
			}
			
			return std::move( new_arr );  
		}
	};
	
	//! deduce implementation class <<IMPL.1>> or <<IMPL.2>>
	typedef typename std::conditional<
		all_ints_or_rngs::value, uniform_get_impl, degenerated_get_impl
	>::type get_impl;
	
	//! public interface to be dispatched according to get_impl
	inline static return_type get( array_type& arr, T id1, ID_types... ids )
	{
		return get_impl::get( arr, id1, ids... );
	}
};

/**
 *  @brief  implementation class for array resizing
 *  @tparam array_type array type
 *  @tparam ...ID_types variadic template arguments of ID types
 *
 *  This class only reset (1)shapes and (2)bases bu does not re-initialize new storage. 
 *  There are three versions of array_resize:
 *  1) array_resize: general type. Never used
 *
 *  2) array_resize<array_type, T>: one-argument version. This is the specialized 
 *  version for one-argument resizing. In this case, only two 
 *  possibilities may be occured: 
 *  (a) a int tiny_vector, such as a( {1,4,2} ), which resizes the array with 1 x 4 x 2
 *  dimension and keeps the bases unmodified.
 *  (b) a range tiny_vector, such as a( {range(3,4), range(2,5), range(1,6)} ), 
 *  which resize the array with 2 x 4 x 7 dimension and changes the bases to (3, 2, 1)
 *
 *  3) array_resize<array_type, T, ID_types...>: many-argument version. This is the
 *  specialized version for many-argument array resize, such as A(4, range(2,5), 8).
 *  For this example, array A will be resized as 4 x 4 x 8 dimension, and for the bases
 *  it will keep the 1st and 3rd dimension same as the original array but change the 2nd
 *  base to 2. For int-range mixed resizing, the implementation is carried out by using
 *  recursive functions to unpack the variadic arguments. According to the type of each
 *  argument, int or range, the unpacked argument will be dispatched to the corresponding
 *  interfaces automatically.
 */
template <typename array_type, class... ID_types>
struct array_resize;

template <typename array_type, class T>
struct array_resize<array_type, T>
{
	inline static void resize( array_type& arr, const tiny_vector<int, array_type::n_dim>& new_shape )
	{
		arr.shape() = new_shape;
	}

	inline static void resize( array_type& arr, const tiny_vector<range, array_type::n_dim>& new_range )
	{
		for( unsigned int dim = 0; dim < array_type::n_dim; dim++ )
		{
			arr.storage().set_base( dim, new_range[dim].start() );
			arr.shape()[dim] = new_range[dim].length();
		}
	}
};

template <typename array_type, class T, class... ID_types>
struct array_resize<array_type, T, ID_types...>
{
	typedef type_list<T, ID_types...> id_types;
	static constexpr unsigned int n_dim = array_type::n_dim;
	
	//! check the length of types (arguments)
	static constexpr bool len_check = (id_types::num_types == n_dim);
	static_assert( len_check, "wrong length of indices");
	
	//! check (num of int) + (num of range) == dimension
	static constexpr unsigned int num_of_int   = num_of_type_inside<int, id_types>::value;
	static constexpr unsigned int num_of_slice = num_of_type_inside<range, id_types>::value;
	static constexpr bool check_equal = (num_of_int + num_of_slice) == n_dim;
	static_assert( check_equal, "indexing sould only be int or range");

	template <class... ID_types_>
	inline static void resize_impl( array_type& arr, unsigned int& cur_dim )
	{}

	template <class... ID_types_>
	inline static void resize_impl( array_type& arr, unsigned int& cur_dim, int len, ID_types_&&... args )
	{
		arr.shape()[cur_dim] = len;
		resize_impl( arr, ++cur_dim, args... );
	}

	template <class... ID_types_>
	inline static void resize_impl( array_type& arr, unsigned int& cur_dim, range r, ID_types_&&... args )
	{
		arr.storage().set_base( cur_dim, r.start() );
		arr.shape()[cur_dim] = r.length();
		resize_impl( arr, ++cur_dim, args... );
	}

	inline static void resize( array_type& arr, T&& arg1, ID_types&&... args )
	{
		unsigned int cur_dim = 0;		
		resize_impl( arr, cur_dim, std::forward<T>(arg1), std::forward<ID_types>(args)... );
	}
};

} //!< array_helper

#endif //!< ARRAY_HELPER